<?php

/**
 * I-Cafe framework
 * 
 * @category CF
 * @package CF_View
 * @link http://www.i-cafe.org
 * @license http://www.i-cafe.org/license
 * @version 04-04-2010
 * @author tcl_java@yahoo.com
 */
class CF_View_Template extends CF_View_Abstract
{
	protected $_includePath;
	
	/**
	 * 
	 * @var Array
	 */
	protected $_fileIncludes = array();
	
	/**
	 * 
	 * @var string
	 */
	protected $_extension = 'html';
	
	/**
	 * 
	 * @var string
	 */
	public $varPattern = '/\{\$([^\{]+)\}/';
	
	public $includeFileParamNamePattern = '/\{\$%s\}/e';
	
	/**
	 * 
	 * @var unknown_type
	 */
	public $includePattern = '/\{include ([\w_\.\/]+)\}/e';
	
	/**
	 * 
	 * @var string
	 */
	public $varBackRefPattern = '/\{((\.\/)*)\.\/\$([\w_]+)\}/';
	
	/**
	 * @var string
	 */
	public $blockFormat = '<!-- B: %s -->%s<!-- E: %s -->';

	/**
	 * 
	 * @var boolean
	 */
	protected $_clearEmpty = true;
	
	/**
	 * 
	 * @var array
	 */
	protected $_blockVars;
	
	public function __construct()
	{
		parent::__construct();
		
		# register events
		$this->addEvents(
			'beforeparse',
			'parse'
		);
		
		$this->_blockVars = new CF_Storage_Array();
	}
	
	/**
	 * 
	 * @param string $path
	 * @return CF_View_Template $this
	 */
	public function setIncludePath($path)
	{
		$this->_includePath = $path;
		return $this;
	}
	
	/**
	 * 
	 * @param string $name
	 * @param string $file
	 * @return CF_View_Template $this
	 */
	public function includeFile($name, $file)
	{
		$this->_fileIncludes[$name] = $file;
		return $this;
	}
	
	/**
	 * 
	 * @param boolean $bol
	 * @return CF_View_Template $this
	 */
	public function clearEmpty($bol)
	{
		$this->_clearEmpty = (boolean) $bol;
		return $this;	
	}
	
	/**
	 * 
	 * @param boolean $bol
	 */
	public function useGlobalVar($bol)
	{
		$this->_useGlobalVar = (boolean) $bol;
	}
	
	/**
	 * 
	 * @param string $name
	 * @param array|null $data
	 * @return CF_View_Template
	 * @throws CF_View_Exception
	 */
	public function parse($name, $data)
	{
		# fire before parse event
		if(CF_Observable::VALUE_RETURN === $this->fireEvent('beforeparse', array($this, &$name, $data))) return;
		
		if(null !== $data && !is_array($data)){
			throw new CF_View_Exception('Parse block must except null or array data.');
		}
		
		$this->_blockVars->set($name, $data);
		
		# fire parse event
		$this->fireEvent('parse', $this, $name, $data);
		
		return $this;
	}
	
	/**
	 * 
	 * @param string $name
	 * @param string $content
	 * @return array|null
	 */
	protected function _parseBlock($name, $content)
	{
		$pattern = sprintf(preg_quote($this->blockFormat, '/'), $name, '(.*)', $name);
		$pattern = '/' . $pattern . '/us';
					
		preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
		if(empty($matches)) return array();
		
		return $matches;
	}
	
	/**
	 * 
	 * @param string $content
	 * @return string
	 */
	protected function _renderBlock($content)
	{
		$temp = new self();
		$temp->varPattern = $this->varPattern;
		$temp->blockFormat = $this->blockFormat;
		$temp->clearEmpty(true);
		
		foreach ($this->_blockVars as $name => $data) {
			$blocks = $this->_parseBlock($name, $content);
			foreach ($blocks as $block) {
				if(current($data) instanceof CF_Storage_Array)
					$data->read(array($data->toArray()));	
				
				$items = '';
				foreach ($data as $item) {
					foreach ($item as $k => $v) {
						if($v instanceof CF_Storage_Array)
							$temp->parse($k, $v->toArray());
						else
							$temp->assign($k, $v);
					}
					$items .= $temp->renderString($block[1]);
				}
				
				$content = str_replace($block[0], $items, $content);
			}
		}
		return $content;
	}
	
	/**
	 * @param string
	 * @return string
	 */
	protected function _render()
	{
		extract($this->_vars->toArray());
		if($this->_resetOnRender) $this->_vars->reset();
		
		ob_start();
		eval('?>' . func_get_arg(0) . '<?php ');
		return ob_get_clean();
	}
	
	/**
	 * 
	 * @param string $content
	 * @return string
	 */
	public function renderString($content)
	{
		# include
		$replace = "file_get_contents('". $this->_includePath ."$1')";
		$content = preg_replace($this->includePattern, $replace, $content);	
		
		# include from include var
		foreach ($this->_fileIncludes as $name => $file) {
			$replace = "file_get_contents('". $this->_includePath . $file . '.' . $this->_extension ."')";
			$content = preg_replace(sprintf($this->includeFileParamNamePattern, $name) . 'e', $replace, $content);
		}
		
		# render block
		$content = $this->_renderBlock($content);
		
		# render var
		$content = preg_replace($this->varPattern, '<?php echo \$$1; ?>', $content);
		
		$content = $this->_render($content);
		
		# reset var on render?
		if($this->_resetOnRender){
			$this->_fileIncludes = array();
			$this->_vars->reset();
		}
		
		# clear empty block
		if($this->_clearEmpty){
			# clear empty block
			$pattern = sprintf(preg_quote($this->blockFormat, '/'), '([\w]+)', '(.*)', '\1');
			$pattern = '/' . $pattern . '/us';
			$content = preg_replace($pattern, '', $content);
		}
		
		# render back reference
		$content = preg_replace($this->varBackRefPattern, '{\1$\3}', $content);
		
		return  $content;
	}
	
	/**
	 * 
	 * @param string $file
	 * @param string $path
	 * @return string
	 * @throws CF_View_Exception
	 */
	public function render()
	{
		# fire before render event
		if(CF_Observable::VALUE_RETURN === $this->fireEvent('beforerender', array($this))) return;
		
		$file = $this->getViewFile();
		$content = @file_get_contents($file);
		if(!$content){
			throw new CF_View_Exception("View script \"$file\" does not exists");
		}
		
		$res = $this->renderString($content);
		
		# fire render event
		$this->fireEvent('render', array($this, &$res));
		
		return $res;
	}
}